﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Threading;
using Cjc.Calculation.ExpressionEngine.Excel.Compiler;
using System.Linq.Expressions;
using Microsoft.Scripting;
using Microsoft.Scripting.Runtime;

namespace Cjc.Calculation.ExpressionEngine
{
	public class Formula : INotifyPropertyChanged
	{
		protected Engine engine;

		private static MethodInfo invokeMethod = typeof( Formula ).GetMethod( "Invoke", BindingFlags.Public | BindingFlags.Instance );

		private string expressionText;
		private Invoker invoker;
		private Formula nullFormula;
		private Formula[] dependencies;
		private Formula[] staticDependencies;
		private List<KeyValuePair<SymbolId, Expression>> variables = new List<KeyValuePair<SymbolId, Expression>>();
		private ParameterExpression[] parameters;

		public Formula( Engine engine )
		{
			this.engine = engine;
		}

		public string Key { get; set; }
		public SymbolId Symbol { get; set; }
		public object Tag { get; set; }
		public string Format { get; set; }
		public object DebugValue { get; set; }

		public void AddVariable( string name, Expression expression )
		{
			AddVariable( SymbolTable.StringToCaseInsensitiveId( name ), expression );
		}

		public void AddVariable( SymbolId symbol, Expression expression )
		{
			lock ( variables ) variables.Add( new KeyValuePair<SymbolId, Expression>( symbol, expression ) );
			Interlocked.Exchange<ParameterExpression[]>( ref parameters, null );
		}

		public string ExpressionText
		{
			get { return expressionText; }
			set
			{
				expressionText = value;
				SetInvoker( null );
			}
		}

		private void EnsureInvoker()
		{
			if ( invoker == null )
			{
				List<Formula> dependencies = new List<Formula>();

				if ( expressionText != null ) SetInvoker( CompileInvoker( expressionText, dependencies ) );

				Interlocked.Exchange<Formula[]>( ref this.dependencies, dependencies.ToArray() );
			}
		}

		public Invoker Invoker
		{
			get
			{
				EnsureInvoker();
				return invoker;
			}

			set
			{
				SetInvoker( value );
			}
		}

		private void SetInvoker( Invoker invoker )
		{
			Interlocked.Exchange<Invoker>( ref this.invoker, invoker );
			OnPropertyChanged( "Invoker" );
		}

		public Invoker MakeInvoker( Expression expression )
		{
			return ( Parameters != null && Parameters.Length > 0 )
				? new ParameterizedInvoker( expression, engine.LanguageContext, Parameters )
				: new Invoker( expression, engine.LanguageContext );
		}

		public Invoker CompileInvoker( string expressionText, IList<Formula> dependencies )
		{
			var code = engine.CompileExpression( expressionText, PreferredType );
			var rewriter = new VariableRewriter( engine, variables.ToDictionary( v => v.Key, v => v.Value ) );
			var invoker = MakeInvoker( rewriter.Visit( code ) );

			if ( dependencies != null )
			{
				foreach ( var d in rewriter.Dependencies.Distinct() ) dependencies.Add( d );
			}

			return invoker;
		}

		public Formula NullFormula
		{
			get { return nullFormula; }
			set { Interlocked.Exchange<Formula>( ref nullFormula, value ); }
		}

		public Type PreferredType { get; set; }

		public ParameterExpression[] Parameters
		{
			get { return variables.Select( v => v.Value ).OfType<ParameterExpression>().ToArray(); }
		}

		public bool HasExpression
		{
			get { return Invoker != null; }
		}

		public Expression Expression
		{
			get { return ( Invoker != null ) ? Invoker.Code : null; }
			set { Invoker = MakeInvoker( value ); }
		}

		public IEnumerable<Formula> Dependencies
		{
			get
			{
				if ( dependencies == null ) EnsureInvoker();
				return dependencies;
			}
		}

		public IEnumerable<Formula> NestedDependencies
		{
			get
			{
				if ( staticDependencies == null ) return new Formula[ 0 ];

				EnsureInvoker();
				return staticDependencies.SelectMany( f => f.Dependencies ).Distinct();
			}
		}

		public object Invoke( WorksheetScope scope, params object[] args )
		{
			EnsureInvoker();

			return ( invoker != null )
				? invoker.Invoke( scope )
				: null;
		}

		public Formula MakeParameterizedFormula( SymbolId symbol, params object[] args )
		{
			var formula = new Formula( engine );
			formula.Symbol = symbol;
			formula.expressionText = ExpressionText;

			formula.invoker = new Invoker( Invoker, args );
			formula.staticDependencies = new[] { this };

			return formula;
		}

		public object Execute( WorksheetScope scope )
		{
			return Invoke( scope );
		}

		public Formula MakeOptimizedFormula( Expression expression )
		{
			var formula = new Formula( engine );
			formula.Symbol = Symbol;
			formula.expressionText = expressionText;
			formula.invoker = MakeInvoker( expression );

			return formula;
		}

		public override string ToString()
		{
			return Symbol.ToString();
		}

		#region INotifyPropertyChanged Members

		public event PropertyChangedEventHandler PropertyChanged;

		protected void OnPropertyChanged( string propertyName )
		{
			if ( PropertyChanged != null ) PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
		}

		#endregion
	}
}